package net.betou.common.web.cos;

import java.io.*;
import java.util.*;
import java.util.Map.Entry;

import javax.servlet.*;
import javax.servlet.http.*;

import net.betou.common.web.RequestUtils;

/**
 * A utility class to handle <code>multipart/form-data</code> requests, the kind
 * of requests that support file uploads. This class emulates the interface of
 * <code>HttpServletRequest</code>, making it familiar to use. It uses a "push"
 * model where any incoming files are read and saved directly to disk in the
 * constructor. If you wish to have more flexibility, e.g. write the files to a
 * database, use the "pull" model <code>MultipartParser</code> instead.
 * <p>
 * This class can receive arbitrarily large files (up to an artificial limit you
 * can set), and fairly efficiently too. It cannot handle nested data (multipart
 * content within multipart content). It <b>can</b> now with the latest release
 * handle internationalized content (such as non Latin-1 filenames).
 * <p>
 * To avoid collisions and have fine control over file placement, there's a
 * constructor variety that takes a pluggable FileRenamePolicy implementation. A
 * particular policy can choose to rename or change the location of the file
 * before it's written.
 * <p>
 * See the included upload.war for an example of how to use this class.
 * <p>
 * The full file upload specification is contained in experimental RFC 1867,
 * available at <a href="http://www.ietf.org/rfc/rfc1867.txt">
 * http://www.ietf.org/rfc/rfc1867.txt</a>.
 * 
 * @see MultipartParser
 * 
 * @author Jason Hunter
 * @author Geoff Soutter
 * @version 1.12, 2004/04/11, added null check for Opera malformed bug<br>
 * @version 1.11, 2002/11/01, combine query string params in param list<br>
 * @version 1.10, 2002/05/27, added access to the original file names<br>
 * @version 1.9, 2002/04/30, added support for file renaming, thanks to
 *          Changshin Lee<br>
 * @version 1.8, 2002/04/30, added support for internationalization, thanks to
 *          Changshin Lee<br>
 * @version 1.7, 2001/02/07, made fields protected to increase user flexibility<br>
 * @version 1.6, 2000/07/21, redid internals to use MultipartParser, thanks to
 *          Geoff Soutter<br>
 * @version 1.5, 2000/02/04, added auto MacBinary decoding for IE on Mac<br>
 * @version 1.4, 2000/01/05, added getParameterValues(), WebSphere 2.x
 *          getContentType() workaround, stopped writing empty "unknown" file<br>
 * @version 1.3, 1999/12/28, IE4 on Win98 lastIndexOf("boundary=") workaround<br>
 * @version 1.2, 1999/12/20, IE4 on Mac readNextPart() workaround<br>
 * @version 1.1, 1999/01/15, JSDK readLine() bug workaround<br>
 * @version 1.0, 1998/09/18<br>
 */
public class CosMultipartRequest{

	private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024; // 1 Meg

	protected Hashtable< String, List< String >> parameters = new Hashtable< String, List< String >>( ); // name
																											// -
																											// Vector
																											// of
																											// values
	protected Map< String, UploadedFile > files = new HashMap< String, UploadedFile >( ); // name
																							// -
																							// UploadedFile

	/**
	 * Constructs a new MultipartRequest to handle the specified request, saving
	 * any uploaded files to the given directory, and limiting the upload size
	 * to 1 Megabyte. If the content is too large, an IOException is thrown.
	 * This constructor actually parses the <tt>multipart/form-data</tt> and
	 * throws an IOException if there's any problem reading or parsing the
	 * request.
	 * 
	 * @param request
	 *            the servlet request.
	 * @param saveDirectory
	 *            the directory in which to save any uploaded files.
	 * @exception IOException
	 *                if the uploaded content is larger than 1 Megabyte or
	 *                there's a problem reading or parsing the request.
	 */
	public CosMultipartRequest( HttpServletRequest request, String saveDirectory ) throws IOException {
		this( request, saveDirectory, DEFAULT_MAX_POST_SIZE );
	}

	/**
	 * Constructs a new MultipartRequest to handle the specified request, saving
	 * any uploaded files to the given directory, and limiting the upload size
	 * to the specified length. If the content is too large, an IOException is
	 * thrown. This constructor actually parses the <tt>multipart/form-data</tt>
	 * and throws an IOException if there's any problem reading or parsing the
	 * request.
	 * 
	 * @param request
	 *            the servlet request.
	 * @param saveDirectory
	 *            the directory in which to save any uploaded files.
	 * @param maxPostSize
	 *            the maximum size of the POST content.
	 * @exception IOException
	 *                if the uploaded content is larger than
	 *                <tt>maxPostSize</tt> or there's a problem reading or
	 *                parsing the request.
	 */
	public CosMultipartRequest( HttpServletRequest request, String saveDirectory, int maxPostSize ) throws IOException {
		this( request, saveDirectory, maxPostSize, null, null );
	}

	/**
	 * Constructs a new MultipartRequest to handle the specified request, saving
	 * any uploaded files to the given directory, and limiting the upload size
	 * to the specified length. If the content is too large, an IOException is
	 * thrown. This constructor actually parses the <tt>multipart/form-data</tt>
	 * and throws an IOException if there's any problem reading or parsing the
	 * request.
	 * 
	 * @param request
	 *            the servlet request.
	 * @param saveDirectory
	 *            the directory in which to save any uploaded files.
	 * @param encoding
	 *            the encoding of the response, such as ISO-8859-1
	 * @exception IOException
	 *                if the uploaded content is larger than 1 Megabyte or
	 *                there's a problem reading or parsing the request.
	 */
	public CosMultipartRequest( HttpServletRequest request, String saveDirectory, String encoding ) throws IOException {
		this( request, saveDirectory, DEFAULT_MAX_POST_SIZE, encoding, null );
	}

	/**
	 * Constructs a new MultipartRequest to handle the specified request, saving
	 * any uploaded files to the given directory, and limiting the upload size
	 * to the specified length. If the content is too large, an IOException is
	 * thrown. This constructor actually parses the <tt>multipart/form-data</tt>
	 * and throws an IOException if there's any problem reading or parsing the
	 * request.
	 * 
	 * @param request
	 *            the servlet request.
	 * @param saveDirectory
	 *            the directory in which to save any uploaded files.
	 * @param maxPostSize
	 *            the maximum size of the POST content.
	 * @param policy
	 *            the rules for renaming in case of file name collisions
	 * @exception IOException
	 *                if the uploaded content is larger than
	 *                <tt>maxPostSize</tt> or there's a problem reading or
	 *                parsing the request.
	 */
	public CosMultipartRequest( HttpServletRequest request, String saveDirectory, int maxPostSize, FileRenamePolicy policy ) throws IOException {
		this( request, saveDirectory, maxPostSize, null, policy );
	}

	/**
	 * Constructs a new MultipartRequest to handle the specified request, saving
	 * any uploaded files to the given directory, and limiting the upload size
	 * to the specified length. If the content is too large, an IOException is
	 * thrown. This constructor actually parses the <tt>multipart/form-data</tt>
	 * and throws an IOException if there's any problem reading or parsing the
	 * request.
	 * 
	 * @param request
	 *            the servlet request.
	 * @param saveDirectory
	 *            the directory in which to save any uploaded files.
	 * @param maxPostSize
	 *            the maximum size of the POST content.
	 * @param encoding
	 *            the encoding of the response, such as ISO-8859-1
	 * @exception IOException
	 *                if the uploaded content is larger than
	 *                <tt>maxPostSize</tt> or there's a problem reading or
	 *                parsing the request.
	 */
	public CosMultipartRequest( HttpServletRequest request, String saveDirectory, int maxPostSize, String encoding ) throws IOException {
		this( request, saveDirectory, maxPostSize, encoding, null );
	}

	/**
	 * Constructs a new MultipartRequest to handle the specified request, saving
	 * any uploaded files to the given directory, and limiting the upload size
	 * to the specified length. If the content is too large, an IOException is
	 * thrown. This constructor actually parses the <tt>multipart/form-data</tt>
	 * and throws an IOException if there's any problem reading or parsing the
	 * request.
	 * 
	 * To avoid file collisions, this constructor takes an implementation of the
	 * FileRenamePolicy interface to allow a pluggable rename policy.
	 * 
	 * @param request
	 *            the servlet request.
	 * @param saveDirectory
	 *            the directory in which to save any uploaded files.
	 * @param maxPostSize
	 *            the maximum size of the POST content.
	 * @param encoding
	 *            the encoding of the response, such as ISO-8859-1
	 * @param policy
	 *            a pluggable file rename policy
	 * @exception IOException
	 *                if the uploaded content is larger than
	 *                <tt>maxPostSize</tt> or there's a problem reading or
	 *                parsing the request.
	 */
	public CosMultipartRequest( HttpServletRequest request, String saveDirectory, int maxPostSize, String encoding, FileRenamePolicy policy ) throws IOException {
		// Sanity check values
		if( request == null )
			throw new IllegalArgumentException( "request cannot be null" );
		if( saveDirectory == null )
			throw new IllegalArgumentException( "saveDirectory cannot be null" );
		if( maxPostSize <= 0 ){
			throw new IllegalArgumentException( "maxPostSize must be positive" );
		}

		// Save the dir
		File dir = new File( saveDirectory );

		// Check saveDirectory is truly a directory
		if( !dir.isDirectory( ) )
			throw new IllegalArgumentException( "Not a directory: " + saveDirectory );

		// Check saveDirectory is writable
		if( !dir.canWrite( ) )
			throw new IllegalArgumentException( "Not writable: " + saveDirectory );

		// Parse the incoming multipart, storing files in the dir provided,
		// and populate the meta objects which describe what we found
		MultipartParser parser = new MultipartParser( request, maxPostSize, true, true, encoding );

		// Some people like to fetch query string parameters from
		// MultipartRequest, so here we make that possible. Thanks to
		// Ben Johnson, ben.johnson@merrillcorp.com, for the idea.
		if( request.getQueryString( ) != null ){
			// Let HttpUtils create a name->String[] structure
			Map< String, String [ ] > queryParameters = RequestUtils.parseQueryString( request.getQueryString( ) );
			// For our own use, name it a name->Vector structure
			for ( Entry< String, String [ ] > entry : queryParameters.entrySet( ) ){
				parameters.put( entry.getKey( ), Arrays.asList( entry.getValue( ) ) );
			}
		}

		Part part;
		while( ( part = parser.readNextPart( ) ) != null ){
			String name = part.getName( );
			if( name == null ){
				throw new IOException( "Malformed input: parameter name missing (known Opera 7 bug)" );
			}
			if( part.isParam( ) ){
				// It's a parameter part, add it to the vector of values
				ParamPart paramPart = ( ParamPart ) part;
				String value = paramPart.getStringValue( );
				List< String > existingValues = parameters.get( name );
				if( existingValues == null ){
					existingValues = new ArrayList< String >( );
					parameters.put( name, existingValues );
				}
				existingValues.add( value );
			}else if( part.isFile( ) ){
				// It's a file part
				FilePart filePart = ( FilePart ) part;
				String fileName = filePart.getFileName( );
				if( fileName != null ){
					filePart.setRenamePolicy( policy ); // null policy is OK
					// The part actually contained a file
					filePart.writeTo( dir );
					files.put( name, new UploadedFile( dir.toString( ), filePart.getFileName( ), fileName, filePart.getContentType( ) ) );
				}else{
					// The field did not contain a file
					files.put( name, new UploadedFile( null, null, null, null ) );
				}
			}
		}
	}

	/**
	 * Constructor with an old signature, kept for backward compatibility.
	 * Without this constructor, a servlet compiled against a previous version
	 * of this class (pre 1.4) would have to be recompiled to link with this
	 * version. This constructor supports the linking via the old signature.
	 * Callers must simply be careful to pass in an HttpServletRequest.
	 * 
	 */
	public CosMultipartRequest( ServletRequest request, String saveDirectory ) throws IOException {
		this( ( HttpServletRequest ) request, saveDirectory );
	}

	/**
	 * Constructor with an old signature, kept for backward compatibility.
	 * Without this constructor, a servlet compiled against a previous version
	 * of this class (pre 1.4) would have to be recompiled to link with this
	 * version. This constructor supports the linking via the old signature.
	 * Callers must simply be careful to pass in an HttpServletRequest.
	 * 
	 */
	public CosMultipartRequest( ServletRequest request, String saveDirectory, int maxPostSize ) throws IOException {
		this( ( HttpServletRequest ) request, saveDirectory, maxPostSize );
	}

	/**
	 * Returns the names of all the parameters as an Enumeration of Strings. It
	 * returns an empty Enumeration if there are no parameters.
	 * 
	 * @return the names of all the parameters as an Enumeration of Strings.
	 */
	public Enumeration< String > getParameterNames( ) {
		return parameters.keys( );
	}

	/**
	 * Returns the names of all the uploaded files as an Enumeration of Strings.
	 * It returns an empty Enumeration if there are no file input fields on the
	 * form. Any file input field that's left empty will result in a FilePart
	 * with null contents. Each file name is the name specified by the form, not
	 * by the user.
	 * 
	 * @return the names of all the uploaded files as an Enumeration of Strings.
	 */
	public Set< String > getFileNames( ) {
		return files.keySet( );
	}

	/**
	 * Returns the value of the named parameter as a String, or null if the
	 * parameter was not sent or was sent without a value. The value is
	 * guaranteed to be in its normal, decoded form. If the parameter has
	 * multiple values, only the last one is returned (for backward
	 * compatibility). For parameters with multiple values, it's possible the
	 * last "value" may be null.
	 * 
	 * @param name
	 *            the parameter name.
	 * @return the parameter value.
	 */
	public String getParameter( String name ) {
		try{
			List< String > values = parameters.get( name );
			if( values == null || values.size( ) == 0 ){
				return null;
			}
			return values.get( values.size( ) - 1 );
		}catch( Exception e ){
			return null;
		}
	}

	/**
	 * Returns the values of the named parameter as a String array, or null if
	 * the parameter was not sent. The array has one entry for each parameter
	 * field sent. If any field was sent without a value that entry is stored in
	 * the array as a null. The values are guaranteed to be in their normal,
	 * decoded form. A single value is returned as a one-element array.
	 * 
	 * @param name
	 *            the parameter name.
	 * @return the parameter values.
	 */
	public String [ ] getParameterValues( String name ) {
		List< String > values = parameters.get( name );
		if( values == null || values.size( ) == 0 ){
			return null;
		}else{
			return values.toArray( new String [ values.size( ) ] );
		}
	}

	/**
	 * Returns the filesystem name of the specified file, or null if the file
	 * was not included in the upload. A filesystem name is the name specified
	 * by the user. It is also the name under which the file is actually saved.
	 * 
	 * @param name
	 *            the html page's file parameter name.
	 * @return the filesystem name of the file.
	 */
	public String getFilesystemName( String name ) {
		try{
			UploadedFile file = files.get( name );
			return file.getFilesystemName( ); // may be null
		}catch( Exception e ){
			return null;
		}
	}

	/**
	 * Returns the original filesystem name of the specified file (before any
	 * renaming policy was applied), or null if the file was not included in the
	 * upload. A filesystem name is the name specified by the user.
	 * 
	 * @param name
	 *            the html page's file parameter name.
	 * @return the original file name of the file.
	 */
	public String getOriginalFileName( String name ) {
		try{
			UploadedFile file = ( UploadedFile ) files.get( name );
			return file.getOriginalFileName( ); // may be null
		}catch( Exception e ){
			return null;
		}
	}

	/**
	 * Returns the content type of the specified file (as supplied by the client
	 * browser), or null if the file was not included in the upload.
	 * 
	 * @param name
	 *            the html page's file parameter name.
	 * @return the content type of the file.
	 */
	public String getContentType( String name ) {
		try{
			UploadedFile file = ( UploadedFile ) files.get( name );
			return file.getContentType( ); // may be null
		}catch( Exception e ){
			return null;
		}
	}

	/**
	 * Returns a File object for the specified file saved on the server's
	 * filesystem, or null if the file was not included in the upload.
	 * 
	 * @param name
	 *            the html page's file parameter name.
	 * @return a File object for the named file.
	 */
	public File getFile( String name ) {
		try{
			UploadedFile file = ( UploadedFile ) files.get( name );
			return file.getFile( ); // may be null
		}catch( Exception e ){
			return null;
		}
	}
}

// A class to hold information about an uploaded file.
//
class UploadedFile{

	private String dir;
	private String filename;
	private String original;
	private String type;

	UploadedFile( String dir, String filename, String original, String type ) {
		this.dir = dir;
		this.filename = filename;
		this.original = original;
		this.type = type;
	}

	public String getContentType( ) {
		return type;
	}

	public String getFilesystemName( ) {
		return filename;
	}

	public String getOriginalFileName( ) {
		return original;
	}

	public File getFile( ) {
		if( dir == null || filename == null ){
			return null;
		}else{
			return new File( dir + File.separator + filename );
		}
	}
}
